-- Globals
AnggaraNothing = AnggaraNothing or {}
do
end

-- module: Base122
--[[
MIT License

Copyright (c) 2016 Kevin Albertson
Copyright (c) 2023 Anggara Yama Putra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
do
  local root = {}

  local tablepack = table.pack
  local tableunpack = table.unpack
  local strbyte = string.byte
  local strchar = string.char

  local function create_iter (len, step, ...)
    step = step or 1
    local args
    local unpack = tableunpack
    if select('#', ...) == 1 then
      local first = ...
      local first_type = type( first )
      if first_type == 'table' then
        args, len = first, len or #first
      elseif first_type == 'string' then
        args, len = first, len or #first
        unpack = strbyte
      end
    end
    args = args or tablepack( ... )
    len = len or args.n
    local pos, nextpos = 1, 1
    return function ()
      if nextpos > len then
        return
      end
      pos, nextpos = nextpos, nextpos + step
      return unpack(args, pos, nextpos-1)
    end, len
  end

  local function uint32_rshift (val, count)
    return ((val % 0x100000000) >> count) | 0
  end

  local function twobytes_to_codepoint (...)
    local b1, b2 = ...
    if b2 then
      return (
        (b1 - 0xC0) * 64
        + (b2 - 0x80)
      )
    end
    return b1
  end

  local ILLEGALS = {
    [0]=0, -- null
    10, -- newline
    13, -- carriage return
    34, -- double quote
    38, -- ampersand
    92, -- backslash
  }
  local ILLEGALS_IDX = {}
  for i = 0, #ILLEGALS do
    ILLEGALS_IDX[ILLEGALS[i]] = i
  end
  -- Uses the illegal index to signify the last two-byte char encodes <= 7 bits.
  local SHORTENED = 7
  local MAX_BUFFER_SIZE = 8192

  local encode
  do
    local function get7 (...)
      local iter = create_iter( nil, nil, ... )
      local bit = 0 -- Points to current bit needed
      local buffer = iter()
      return function ()
        if not buffer then
          return
        end
        -- Shift, mask, unshift to get first part.
        local first_part = (uint32_rshift(254, bit) & buffer) << bit
        -- Align it to a seven bit chunk.
        first_part = first_part >> 1
        -- Check if we need to go to the next byte for more bits.
        bit = bit + 7
        if bit < 8 then
          -- Do not need next byte.
          return first_part
        end
        bit = bit - 8
        buffer = iter()
        -- Now we want bits [0..curBit] of the next byte if it exists.
        if not buffer then
          return first_part
        end
        local second_part = (uint32_rshift(0xFF00, bit) & buffer) & 0xFF
        -- Align it.
        second_part = second_part >> (8 - bit)
        return first_part | second_part
      end
    end

    local echunks = {}
    encode = function (...)
      local iter = get7( ... )
      local i = 1
      for bits in iter do
        local illegal_id = ILLEGALS_IDX[bits]
        if illegal_id then
          -- Since this will be a two-byte character, get the next chunk of seven bits.
          local next_bits = iter()

          local b1, b2 = 194, 128
          if not next_bits then
            b1 = b1 | ((7 & SHORTENED) << 2)
            next_bits = bits -- Encode these bits after the shortened signifier.
          else
            b1 = b1 | ((7 & illegal_id) << 2)
          end

          -- Push first bit onto first byte, remaining 6 onto second.
          local firstBit = (next_bits & 64) > 0 and 1 or 0
          b1 = b1 | firstBit
          b2 = b2 | next_bits & 63
          echunks[i] = b1
          echunks[i+1] = b2
          i = i + 2
        else
          echunks[i] = bits
          i = i + 1
        end
      end
      i = i - 1

      local result = echunks
      if #echunks > MAX_BUFFER_SIZE then
        echunks = {}
      end
      return tableunpack(result, 1, i)
    end
  end

  local decode
  do
    local dchunks = {}
    local function push7 ()
      local byte = 0
      local bit = 0
      local i = 1
      return function (v)
        v = v << 1
        -- Align this byte to offset for current byte.
        byte = byte | uint32_rshift(v, bit)
        bit = bit + 7
        if bit >= 8 then
          dchunks[i] = byte
          i = i + 1
          bit = bit - 8
          -- Now, take the remainder, left shift by what has been taken.
          byte = (v << (7 - bit)) & 255
        end
        return i
      end
    end

    decode = function (...)
      local append = push7()
      local i

      local iter = create_iter( nil, nil, ... )
      for c in iter do
        -- Check if this is a two-byte character.
        if c > 127 then
          c = twobytes_to_codepoint( c, iter() )
          -- Note, the charCodeAt will give the codePoint, thus
          -- 0b110xxxxx 0b10yyyyyy will give => xxxxxyyyyyy
          local illegal_id = uint32_rshift(c, 8) & 7 -- 7 = 0b111.
          -- We have to first check if this is a shortened two-byte character, i.e. if it only
          -- encodes <= 7 bits.
          if illegal_id ~= SHORTENED then
            i = append( ILLEGALS[illegal_id] )
          end
          -- Always push the rest.
          i = append( c & 127 )
        else
          -- One byte characters can be pushed directly.
          i = append( c )
        end
      end
      i = i - 1

      local result = dchunks
      if #dchunks > MAX_BUFFER_SIZE then
        dchunks = {}
      end
      return tableunpack(result, 1, i)
    end
  end

  function root.encode_as_string (...)
    return strchar( encode(...) )
  end

  function root.decode_as_string (...)
    return strchar( decode(...) )
  end

  root.encode = encode
  root.decode = decode
  AnggaraNothing.Base122 = root

  --[[ tests
  do
    local samples = {
      {"мир привет!", {104,47,26,11,70,70,194,160,104,47,122,24,6,66,113,80,89,52,22,93,12,8,66}},
      {"hello world!", {52,25,45,70,99,60,64,119,55,215,141,70,33,4}},
      {"🌲🌳🌾🌽", {120,39,113,75,23,66,63,12,89,124,19,120,101,123,97,31,70,47,32}},
    }

    local tableconcat = table.concat
    local debug = log or print
    local rencode, rdecode, rencode_as_string,
    rdecode_as_string = root.encode, root.decode, root.encode_as_string, root.decode_as_string

    for _,v in ipairs(samples) do
      local textstr, outbytes = tableunpack( v )
      local textbytes, outstr = tablepack(textstr:byte(1,-1)), strchar( tableunpack(outbytes) )

      debug( '---Bytes Table---' )
      local encoded = {rencode( textbytes )}
      local encodedstr = strchar( tableunpack(encoded) )
      debug( textstr, ' : ', tableconcat(encoded, ',') )
      assert( encodedstr == outstr, '[tb] encoder failed' )

      local decoded = {rdecode( outbytes )}
      local decodedstr = strchar( tableunpack(decoded) )
      debug( textstr, ' : ', tableconcat(decoded, ',') )
      assert( decodedstr == textstr, '[tb] decoder failed' )

      decoded = {rdecode( encoded )}
      decodedstr = strchar( tableunpack(decoded) )
      debug( decodedstr, ' : ', tableconcat(decoded, ',') )
      assert( decodedstr == textstr, '[tb] roundtrip failed' )

      debug( '---String---' )
      encodedstr = rencode_as_string( textstr )
      debug( textstr, ' : ', encodedstr )
      assert( encodedstr == outstr, '[str] encoder failed' )

      decodedstr = rdecode_as_string( outbytes )
      debug( textstr, ' : ', decodedstr )
      assert( decodedstr == textstr, '[str] decoder failed' )

      decodedstr = rdecode_as_string( encodedstr )
      debug( encodedstr, ' : ', decodedstr )
      assert( decodedstr == textstr, '[str] roundtrip failed' )

      debug( '---Varags---' )
      encoded = {rencode( tableunpack(textbytes) )}
      encodedstr = rencode_as_string( tableunpack(textbytes) )
      debug( textstr, ' : ', tableconcat(encoded, ',') )
      assert( encodedstr == outstr, '[va] encoder failed' )

      decoded = {rdecode( tableunpack(outbytes) )}
      decodedstr = rdecode_as_string( tableunpack(outbytes) )
      debug( textstr, ' : ', tableconcat(decoded, ',') )
      assert( decodedstr == textstr, '[va] decoder failed' )

      decoded = {rdecode( tableunpack(encoded) )}
      decodedstr = strchar( tableunpack(decoded) )
      debug( decodedstr, ' : ', tableconcat(decoded, ',') )
      assert( decodedstr == textstr, '[va] roundtrip failed' )
    end
  end
  --]]
end
